#include "stdafx.h"
#include "Camera.h"

#include "Toolkit.h"

namespace Framework {

Camera::Camera(IDirect3DDevice9* graphicsDevice) : graphicsDevice(graphicsDevice) {
    this->lookAt = D3DXVECTOR3(0.0f, 0.0f, 1.0f);
    this->mirror = false;
    this->moveSpeed = 1.0f;
    this->position = D3DXVECTOR3(0.0f, 0.0f, 0.0f);
    this->rightVector = D3DXVECTOR3(1.0f, 0.0f, 0.0f);
    this->upVector = D3DXVECTOR3(0.0f, 1.0f, 0.0f);
    this->targetPosition = D3DXVECTOR3(0.0f, 0.0f, 0.0f);
    this->targetYawPitchRoll = D3DXVECTOR3(0.0f, 0.0f, 0.0f);
    this->yawPitchRoll = D3DXVECTOR3(0.0f, 0.0f, 0.0f);
    this->yawPitchRollSpeed = 1.0f;
    this->visibilityRange = 100.0f;
    this->graphicsDevice->GetViewport(&this->viewPort);
}

Camera::~Camera(void) {
}

Frustum Camera::GetFrustum() const {
    return this->frustum;
}

void Camera::SetFrustum(Frustum val) {
    this->frustum = val;
}

bool Camera::GetMirror() const {
    return this->mirror;
}

void Camera::SetMirror(bool val) {
    this->mirror = val;
}

FLOAT Camera::GetMoveSpeed() const {
    return this->moveSpeed;
}

void Camera::SetMoveSpeed(FLOAT val) {
    this->moveSpeed = val;
}

FLOAT Camera::GetYawPitchRollSpeed() const {
    return this->yawPitchRollSpeed;
}

void Camera::SetYawPitchRollSpeed(FLOAT val) {
    this->yawPitchRollSpeed = val;
}

FLOAT Camera::GetVisibilityRange() const {
    return this->visibilityRange;
}

void Camera::SetVisibilityRange(FLOAT val) {
    this->visibilityRange = val;
}

D3DXVECTOR3 Camera::GetLookAt() const {
    return this->lookAt;
}

void Camera::SetLookAt(D3DXVECTOR3 lookAt) {
    this->lookAt = lookAt;
    D3DXVec3Cross(&this->rightVector, &this->upVector, &this->lookAt);
    D3DXVec3Normalize(&this->rightVector, &this->rightVector);
}

D3DXVECTOR3 Camera::GetPosition() {
    return this->position;
}

void Camera::SetPosition(D3DXVECTOR3 position) {
    this->position = position;
    D3DXVec3Cross(&this->rightVector, &this->upVector, &this->lookAt);
    D3DXVec3Normalize(&this->rightVector, &this->rightVector);
}

D3DXVECTOR3 Camera::GetUpVector() const {
    return this->upVector;
}

void Camera::SetUpVector(D3DXVECTOR3 val) {
    this->upVector = val;
    D3DXVec3Cross(&this->rightVector, &this->upVector, &this->lookAt);
    D3DXVec3Normalize(&this->rightVector, &this->rightVector);
}

void Camera::SetProjectionMatrix(D3DXMATRIX projection) {
    this->projection = projection;
}

D3DXMATRIX Camera::GetProjectionMatrix() {
    return this->projection;
}

D3DXMATRIX Camera::GetViewMatrix() {
    // Keep camera's axes orthogonal to each other
    D3DXVec3Normalize(&this->lookAt, &this->lookAt);

    D3DXVec3Cross(&this->upVector, &this->lookAt, &this->rightVector);
    D3DXVec3Normalize(&this->upVector, &this->upVector);

    D3DXVec3Cross(&this->rightVector, &this->upVector, &this->lookAt);
    D3DXVec3Normalize(&this->rightVector, &this->rightVector);

    // Build the view matrix:
    float x = -D3DXVec3Dot(&this->rightVector, &this->position);
    float y = -D3DXVec3Dot(&this->upVector, &this->position);
    float z = -D3DXVec3Dot(&this->lookAt, &this->position);

    D3DXMATRIX viewMatrix;
    viewMatrix(0, 0) = this->rightVector.x;
    viewMatrix(1, 0) = this->rightVector.y;
    viewMatrix(2, 0) = this->rightVector.z;
    viewMatrix(3, 0) = x;
    viewMatrix(0, 1) = this->upVector.x;
    viewMatrix(1, 1) = this->upVector.y;
    viewMatrix(2, 1) = this->upVector.z;
    viewMatrix(3, 1) = y;
    viewMatrix(0, 2) = this->lookAt.x;
    viewMatrix(1, 2) = this->lookAt.y;
    viewMatrix(2, 2) = this->lookAt.z;
    viewMatrix(3, 2) = z;
    viewMatrix(0, 3) = 0.0f;
    viewMatrix(1, 3) = 0.0f;
    viewMatrix(2, 3) = 0.0f;
    viewMatrix(3, 3) = 1.0f;

    if (this->mirror) {
        D3DXMATRIX mirror;
        D3DXMatrixIdentity(&mirror);
        mirror._22 = -1;
        mirror._42 = 4;
        return mirror * viewMatrix;
    }
    return viewMatrix;
}

void Camera::Strafe(FLOAT units) {
    this->position += D3DXVECTOR3(this->rightVector.x, 0.0f, this->rightVector.z) * units;
}

void Camera::BuildViewFrustum() {
    D3DXMATRIX viewProjection = this->GetViewMatrix() * this->GetProjectionMatrix();
    // Left plane
    this->frustum.Left.a = viewProjection._14 + viewProjection._11;
    this->frustum.Left.b = viewProjection._24 + viewProjection._21;
    this->frustum.Left.c = viewProjection._34 + viewProjection._31;
    this->frustum.Left.d = viewProjection._44 + viewProjection._41;
    D3DXPlaneNormalize(&this->frustum.Left, &this->frustum.Left);

    // Right plane
    this->frustum.Right.a = viewProjection._14 - viewProjection._11;
    this->frustum.Right.b = viewProjection._24 - viewProjection._21;
    this->frustum.Right.c = viewProjection._34 - viewProjection._31;
    this->frustum.Right.d = viewProjection._44 - viewProjection._41;
    D3DXPlaneNormalize(&this->frustum.Right, &this->frustum.Right);

    // Top plane
    this->frustum.Top.a = viewProjection._14 - viewProjection._12;
    this->frustum.Top.b = viewProjection._24 - viewProjection._22;
    this->frustum.Top.c = viewProjection._34 - viewProjection._32;
    this->frustum.Top.d = viewProjection._44 - viewProjection._42;
    D3DXPlaneNormalize(&this->frustum.Top, &this->frustum.Top);

    // Bottom plane
    this->frustum.Bottom.a = viewProjection._14 + viewProjection._12;
    this->frustum.Bottom.b = viewProjection._24 + viewProjection._22;
    this->frustum.Bottom.c = viewProjection._34 + viewProjection._32;
    this->frustum.Bottom.d = viewProjection._44 + viewProjection._42;
    D3DXPlaneNormalize(&this->frustum.Bottom, &this->frustum.Bottom);

    // Near plane
    this->frustum.Near.a = viewProjection._13;
    this->frustum.Near.b = viewProjection._23;
    this->frustum.Near.c = viewProjection._33;
    this->frustum.Near.d = viewProjection._43;
    D3DXPlaneNormalize(&this->frustum.Near, &this->frustum.Near);

    // Far plane
    this->frustum.Far.a = viewProjection._14 - viewProjection._13;
    this->frustum.Far.b = viewProjection._24 - viewProjection._23;
    this->frustum.Far.c = viewProjection._34 - viewProjection._33;
    this->frustum.Far.d = viewProjection._44 - viewProjection._43;
    D3DXPlaneNormalize(&this->frustum.Far, &this->frustum.Far);
}

FrustumCullingType Camera::BoxInFrustum(const d3d::BoundingBox* boundingBox) {
    int iTotalIn = 0;

    // get the corners of the box into the vCorner array
    D3DXVECTOR4 vertices[8];
    Toolkit::GetVertices(boundingBox, &vertices[0]);

    // test all 8 corners against the 6 sides
    // if all points are behind 1 specific plane, we are out
    // if we are in with all points, then we are fully in
    for (int p = 0; p < 6; ++p) {
        int iInCount = 8;
        int iPtIn = 1;

        for (int i = 0; i < 8; ++i) {

            // test this point against the planes
            float distance = D3DXPlaneDot(this->frustum.Planes[p], &vertices[i]);
            if (distance < 0) {
                iPtIn = 0;
                --iInCount;
            }
        }

        // were all the points outside of plane p?
        if (iInCount == 0) {
            return FRUSTUMCULLINGTYPE_NONE;
        }

        // check if they were all on the right side of the plane
        iTotalIn += iPtIn;
    }

    // so if iTotalIn is 6, then all are inside the view
    if (iTotalIn == 6) {
        return FRUSTUMCULLINGTYPE_FULL;
    }

    // we must be partly in then otherwise
    return FRUSTUMCULLINGTYPE_PARTIAL;
}

void Camera::MoveTo(D3DXVECTOR3 position) {
    this->targetPosition = position;
}

void Camera::Project(D3DXVECTOR3* out, const D3DXVECTOR3* vector, const D3DXMATRIX* world) {
    D3DXMATRIX worldViewProjection = *world * this->GetViewMatrix() * this->GetProjectionMatrix();
    D3DXVec3Project(out, vector, &this->viewPort, &this->GetProjectionMatrix(), &this->GetViewMatrix(), world);
    return;
}

FrustumCullingType Camera::SphereInFrustum(const d3d::BoundingSphere* boundingSphere) {
    D3DXVECTOR4 position4 = D3DXVECTOR4(boundingSphere->_center, 1.0f);

    // various distances
    FLOAT fDistance;

    // calculate our distances to each of the planes
    for (int i = 0; i < 6; ++i) {
        // find the distance to this plane
        fDistance = D3DXPlaneDot(this->frustum.Planes[i], &position4);

        // if this distance is < -sphere.radius, we are outside
        if (fDistance < -boundingSphere->_radius) {
            return FRUSTUMCULLINGTYPE_NONE;
        }

        // else if the distance is between +- radius, then we intersect
        if ((float)fabs(fDistance) < boundingSphere->_radius) {
            return FRUSTUMCULLINGTYPE_PARTIAL;
        }
    }

    // otherwise we are fully in view
    return FRUSTUMCULLINGTYPE_FULL;
}

FrustumCullingType Camera::SphereInRadius(const d3d::BoundingSphere* boundingSphere, FLOAT radius) {
    FLOAT distance = D3DXVec3Length(&(this->position - boundingSphere->_center));
    FLOAT neardistance = distance - boundingSphere->_radius;
    FLOAT fardistance = distance + boundingSphere->_radius;
    if (fardistance < radius) {
        return FRUSTUMCULLINGTYPE_FULL;
    } else if (fardistance > radius && neardistance < radius) {
        return FRUSTUMCULLINGTYPE_PARTIAL;
    } else if (neardistance > radius) {
        return FRUSTUMCULLINGTYPE_NONE;
    }

    return FRUSTUMCULLINGTYPE_NONE;
}

FrustumCullingType Camera::SphereInVisibleRadius(const d3d::BoundingSphere* boundingSphere) {
    return this->SphereInRadius(boundingSphere, this->visibilityRange);
}

void Camera::Turn(FLOAT angle) {
    D3DXMATRIX t;
    D3DXMatrixRotationY(&t, angle);
    D3DXVec3TransformCoord(&this->rightVector, &this->rightVector, &t);
    D3DXVec3TransformCoord(&this->lookAt, &this->lookAt, &t);
}

void Camera::Update(FLOAT timeDelta) {
    D3DXVECTOR3 v = this->targetPosition - this->position;
    FLOAT distance = D3DXVec3Length(&v);
    if (distance != 0) {
        FLOAT x = this->moveSpeed * timeDelta;

        if (distance <= x) {
            this->position = this->targetPosition;
        } else {
            D3DXVec3Normalize(&v, &v);
            v *= x;
            this->position += v;
        }
    }

    D3DXVECTOR3 r = this->targetYawPitchRoll - this->yawPitchRoll;
    FLOAT delta = D3DXVec3Length(&r);
    if (delta != 0) {
        FLOAT t = this->yawPitchRollSpeed * timeDelta;
        D3DXVec3Normalize(&r, &r);
        if (delta <= t) {
            r *= delta;
            this->yawPitchRoll = this->targetYawPitchRoll;
        } else {
            r *= t;
            this->yawPitchRoll += r;
        }

        D3DXMATRIX matrixRotation;
        D3DXMatrixRotationYawPitchRoll(&matrixRotation, r.y, r.x, r.z);
        D3DXVECTOR4 lookAtR;
        D3DXVec3Transform(&lookAtR, &this->lookAt, &matrixRotation);
        this->lookAt = D3DXVECTOR3(lookAtR.x, lookAtR.y, lookAtR.z);
        D3DXVECTOR4 upVectorR;
        D3DXVec3Transform(&upVectorR, &this->upVector, &matrixRotation);
        this->upVector = D3DXVECTOR3(upVectorR.x, upVectorR.y, upVectorR.z);
    }
}

void Camera::Walk(FLOAT units) {
    this->position += D3DXVECTOR3(this->lookAt.x, 0.0f, this->lookAt.z) * units;
}

void Camera::YawPitchRollTo(D3DXVECTOR3 yawPitchRoll) {
    this->targetYawPitchRoll = yawPitchRoll;
}

}
